package apps.sim.action.handler;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import javax.xml.soap.MessageFactory;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPConnection;
import javax.xml.soap.SOAPConnectionFactory;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPFactory;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import apps.sim.core.log.Console;
import apps.sim.core.log.Console.Color;
import apps.sim.core.log.Logger;
import apps.sim.util.Base64;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;

public class CacheResponseHandler extends CannedFileHandler {
	private static final String SHYCOP_CACHE = "shycop-cache";
	private static final String ENCODING = "UTF-8";
	private static final String DIGEST_ALGO = "SHA-256";

	public String getDescription() {
		return "This is almost similar to CannedFileHandler, reads canned files based on configured parameters. "
				+ "But, this also creates canned responsed from the actual host for all different requests. " + "Requests are separated by the combination of configured keys";
	}

	public byte[] handle(HttpExchange exchange, String context) throws Exception {
		final InputStream requestBody = exchange.getRequestBody();
		final String req = readInput(requestBody);
		final File file = getFile(context, encode(getCannedFile(context, req)));
		if (file.exists() && !cacheRefreshNeeded(file)) {
			return asByteArray(file);
		} else {
			String actual = getHandlerConfiguration().getActualURL();
			if (actual == null) {
				throw new RuntimeException("No actual URL configuration found, please change the settings");
			}
			String response = gotoActual(actual, req, exchange.getRequestHeaders());
			final FileWriter fileWriter = new FileWriter(file);
			fileWriter.write(response);
			fileWriter.close();
			return response.getBytes();
		}
	}

	private boolean cacheRefreshNeeded(File file) {
		final int refreshTime = getHandlerConfiguration().getRefreshTimeInSeconds();
		if (refreshTime != -1 && file.lastModified() < (System.currentTimeMillis() - 1000 * refreshTime)) {
			Console.print("Refreshing cache after " + refreshTime + " seconds.", Color.WARNING);
			return true;
		}
		return false;
	}

	/**
	 * 
	 * @param file
	 * @return
	 * @throws UnsupportedEncodingException
	 * @throws NoSuchAlgorithmException
	 */
	private String encode(String file) throws UnsupportedEncodingException, NoSuchAlgorithmException {
		final MessageDigest md = MessageDigest.getInstance(DIGEST_ALGO);
		md.update(file.getBytes(ENCODING));
		final byte[] digest = md.digest();
		final String encode = Base64.encode(digest);
		return URLEncoder.encode(encode, ENCODING);
	}

	public File getFile(final String context, String file) {
		final CannedHandlerConfiguration conf = getHandlerConfiguration();
		String root = (conf != null && conf.getRootDirectory() != null) ? conf.getRootDirectory() : null;
		final String folder = root + SEPARATOR + SHYCOP_CACHE + SEPARATOR;
		try {
			new File(folder).mkdirs();
		} catch (Exception e) {

		}
		final String name = folder + file;
		final File absFile = new File(name);
		if (!absFile.exists()) {
			Console.print("Actual file not found, SHyCoP is going to call actual service and cache the data.", Color.WARNING);
			Console.print("Cache Location: " + folder + " || Key: " + file, Color.WARNING);

		}
		return absFile;
	}

	private String gotoActual(String url, String req, Headers headers) throws Exception {
		final SOAPConnectionFactory soapConnFactory = SOAPConnectionFactory.newInstance();
		final SOAPConnection connection = soapConnFactory.createConnection();
		final MessageFactory messageFactory = MessageFactory.newInstance();
		final SOAPMessage message = messageFactory.createMessage();
		final Set<Entry<String, List<String>>> entrySet = headers.entrySet();
		final SOAPHeader soapHeader = message.getSOAPHeader();
		for (Entry<String, List<String>> entry : entrySet) {
			final String key = entry.getKey();
			final Name name = SOAPFactory.newInstance().createName(key, soapHeader.getPrefix(), soapHeader.getNamespaceURI());
			final SOAPElement addChildElement = soapHeader.addChildElement(name);
			final String textContent = entry.getValue() != null ? entry.getValue().get(0) : null;
			addChildElement.setTextContent(textContent);
			message.getMimeHeaders().addHeader(entry.getKey(), textContent);
		}
		final SOAPPart soapPart = message.getSOAPPart();
		final StreamSource preppedMsgSrc = new StreamSource(new ByteArrayInputStream(req.getBytes()));
		soapPart.setContent(preppedMsgSrc);
		message.saveChanges();
		Console.print("Actual URL hit: " + url, Color.INFO);
		final SOAPMessage reply = connection.call(message, url);
		final Source sourceContent = reply.getSOAPPart().getContent();
		final StringWriter writer = new StringWriter();
		final StreamResult result = new StreamResult(writer);
		final TransformerFactory transformerFactory = TransformerFactory.newInstance();
		final Transformer transformer = transformerFactory.newTransformer();
		transformer.transform(sourceContent, result);
		connection.close();
		final String response = writer.toString();
		Console.print("INCOMING message from: " + url, Color.INFO);

		Logger.log(response);
		return response;

	}

	private CacheCannedHandlerConfiguration configuration = new CacheCannedHandlerConfiguration();

	@Override
	public CacheCannedHandlerConfiguration getHandlerConfiguration() {
		return configuration;
	}

	public void setHandlerConfiguration(IHandlerConfig config) {
		this.configuration = (CacheCannedHandlerConfiguration) config;
	}

	public static class CacheCannedHandlerConfiguration extends CannedHandlerConfiguration {
		private String actualURL;
		private int refreshTimeInSeconds = -1;

		public String getActualURL() {
			return actualURL;
		}

		@Config(descrition = "SHyCoP will first search for a canned file, if not present it will call actual endpoint and then store against a key based on confiugred attributes.")
		public void setActualURL(String simulatorVsActualURLMapping) {
			this.actualURL = simulatorVsActualURLMapping;
		}

		public int getRefreshTimeInSeconds() {
			return refreshTimeInSeconds;
		}

		@Config(descrition = "Cache refresh time in seconds. After this period, SHyCoP will call the actual service to replace the cached data")
		public void setRefreshTimeInSeconds(int refreshTimeInSeconds) {
			this.refreshTimeInSeconds = refreshTimeInSeconds;
		}

		@Action(descrition = "This will clear cached files. If flag is false then this will only clear expired files or everything")
		public String cleanCache() {
			int count = 0;
			final String root = getRootDirectory();
			if (root != null) {
				final String folder = root + SEPARATOR + SHYCOP_CACHE + SEPARATOR;
				final File file = new File(folder);
				final File[] listFiles = file.listFiles();
				if (listFiles != null) {
					for (File cache : listFiles) {
						if (cache.lastModified() < (System.currentTimeMillis() - 1000 * this.getRefreshTimeInSeconds())) {
							cache.delete();
							count++;
						}
					}
				}
			}
			return count + " file(s) deleted";
		}

		@Override
		public CacheCannedHandlerConfiguration cloneInstance() {
			final CacheCannedHandlerConfiguration config = new CacheCannedHandlerConfiguration();
			config.setRootDirectory(this.getRootDirectory());
			config.setCommaSeparatedAttributes(this.getCommaSeparatedAttributes());
			config.setSoapRequest(this.isSoapRequest());
			config.setActualURL(this.getActualURL());
			config.setRefreshTimeInSeconds(this.getRefreshTimeInSeconds());
			return config;
		}

		@Override
		public void fromString(String string) {
			String[] split = string.split(ARG_SEPARATOR_REGEX);
			this.setRootDirectory(split[0]);
			this.setCommaSeparatedAttributes(split[1]);
			this.setSoapRequest(Boolean.parseBoolean(split[2]));
			this.setActualURL(split[3]);
			this.setRefreshTimeInSeconds(Integer.parseInt(split[4]));
		}

		@Override
		public String asString() {
			return getRootDirectory() + ARG_SEPARATOR + getCommaSeparatedAttributes() + ARG_SEPARATOR + isSoapRequest() + ARG_SEPARATOR + getActualURL() + ARG_SEPARATOR
					+ getRefreshTimeInSeconds();
		}

	}
}
